package d20chat;

import java.util.Stack;
import java.util.Scanner;
import java.util.Random;


/**
 * Evaulates a math string and outputs a string with solution.
 *    supports:
 *    binary +, -, *, and / 
 *    () 
 *    dice rolls in the form of #d# where the first number is the amount of dice
 *        and the second number is the number of sides
 *    If dice rolls included the rolls will be included in the solution string
 */
public class StringMath {
    
    
    /********************************************************************************
      Method: convert
      Description: converts an infix math string into a postfix math string
      Parameters: String expression = the infix math string
      Used by: StringMath.eval()
      Uses:
      Returns: postfix math string
      Exceptions: ArithmaticException if the math is unrecognized
    ********************************************************************************/
    private String convert(String expression)
    {
        //Initialize an empty stack (string stack), prepare input infix expression and clear postfix string
        Stack<Object> theStack = new Stack<Object>();
        String postfix = "";
        
        char token; //holds each character in the infix string as we parse it
        String element;       
        char temp;

        //Repeat until we reach end of infix expression
        int i = 0;
        boolean lastNum = false;
        boolean decimal = false;
        
        while( i < expression.length())
        {
            //Get token
            token = expression.charAt(i);
        
            if( lastNum && !isNumber(token ) )
            {
                postfix = postfix + " ";
                lastNum = false;
                decimal = false;
            }            
            
           if( token == ' ' )
           {
                //it's okay for users to put in spaces, but we ignore them
           }
            else if ( isOpen(token) )
           {                
                    //Left parenthesis: Push it into stack
                    theStack.push(token);
           }
           else if( isClose(token) )
           {
                element = ".";
                    //Right parenthesis: Keep popping from the stack and appending to postfix string until 
                    //we reach the left parenthesis.
                    //If stack becomes empty and we didn't reach the left parenthesis
                    if( !theStack.empty() )
                    {
                        element = theStack.pop().toString();

                        while( !isOpen(element.charAt(0)) && !theStack.empty() )
                        {
                            postfix = postfix + element + " ";
                            element = theStack.pop().toString();

                        }
                    }
                
                    if( theStack.empty() && !isOpen(element.charAt(0)) )
                    {
                        //error
                        throw new ArithmeticException("unbalanced parentheses : extra ')'");
                    }
           }
           else if( isOperator(token) )
           {
                //Operator: If stack is empty or operator has a higher precedence than the top of the stack 
                //then push operator into stack. 
                if( theStack.empty() )
                {
                    theStack.push(token);
                }
                else if( getPrecedence(token) > getPrecedence( theStack.peek().toString().charAt(0) ) )
                {
                    theStack.push( token );
                }
                else
                {
                 //Else if operator has lower precedence then we keep popping and appending to postfix string,
                //this is repeated until operator in stack has lower precedence than the current operator.
                    while(  !theStack.empty() && (getPrecedence(token) <= getPrecedence( theStack.peek().toString().charAt(0))) )
                    {
                        postfix = postfix + theStack.pop().toString() + " ";
                    }
   
                    theStack.push( token );
                    
                }   
           }
           else if( isNumber(token) )
           {
                lastNum = true;
                //An operand: we simply append it to postfix string. 
                //need to get the full operand here
               if( token == '.' )
               {
                    if( decimal == true )
                    {
                        //error
                        throw new ArithmeticException("invalid number");
                    }
                    else
                    {
                        decimal = true;
                    }
               }
                
                postfix = postfix + token;
         
            }
            else
            {
                //error
                throw new ArithmeticException("unknown symbol : " + token);
            }
            i++;
        }
            if( lastNum )
            {
                postfix = postfix + " ";
            }
        //When the infix expression is finished, we start popping off the stack and appending to postfix string till stack becomes empty.      
        while( !theStack.empty() )
        {
            element = theStack.pop().toString();
           
           if( !isOpen(element.charAt(0)) )
           {
                //error
                throw new ArithmeticException("unbalanced parentheses : extra '('");
           }
            postfix = postfix + element + " ";
        }
        
        return postfix;
    }
   
    
    /********************************************************************************
      Method: evaluatePostfix
      Description: finds a solution to a postfix math expression
      Parameters: String postfix = a postfix math string
      Used by: StringMath.eval()
      Uses:
      Returns: solution including any dice rolls
      Exceptions: throws Arithmatic exception if math is bad or unrecognized
    ********************************************************************************/
    private String evaluatePostfix( String postfix )
    {
        String value = "";
        float firstOperand;
        float secondOperand;
        float temp;
        String element;
        Stack<Object> theStack = new Stack<Object>();
        Random generator = new Random();
        int randomNumber;
        int total;
        float sum;
        String dice;
        
        Scanner theScanner = new Scanner( postfix );
        theScanner.useDelimiter( "\\s" );
        
        while( theScanner.hasNext() )
        {
          element = theScanner.next();
           
          if( isOperator( element.charAt(0) ) )
          {
              if( theStack.isEmpty() )
              {
                    throw new ArithmeticException("operator without two operands");
              }
              else
              {
                    secondOperand = Float.parseFloat( theStack.pop().toString() );
              }
              
              if( theStack.isEmpty() )
              {
                    throw new ArithmeticException("operator missing an operand");
              }
              else
              {
                firstOperand = Float.parseFloat( theStack.pop().toString() );
                switch ( element.charAt(0) )
                {
                    case '+' : 
                        temp = firstOperand + secondOperand;
                        theStack.push(temp);
                        break;
                    case '-' : 
                        temp = firstOperand - secondOperand;
                        theStack.push(temp);
                        break; 
                    case '*' : 
                        temp = firstOperand * secondOperand;
                        theStack.push(temp);
                        break;
                    case '/' : 
                        temp = firstOperand / secondOperand;
                        theStack.push(temp);
                        break;
                    case 'd' :
                        int diceType = (int)secondOperand;
                        total = 0;
                        dice = "";
                        if( diceType == 0)
                        {
                            //if they are rolling a number of 0 dice
                            //then return them the 0 in total
                        }
                        else
                        {
                            value = value + " -> " + (int)firstOperand + "d" + diceType + " : ";
                            for( int i = 0; i < (int)firstOperand; i++  )
                            {

                                randomNumber = generator.nextInt( diceType ) + 1;
                                total += randomNumber;
                                value = value + randomNumber + " ";
                                dice += Integer.toString(randomNumber) + " ";                
                            }
                        }
                        theStack.push(total);

                        break;
                    }
              }
            
          }
          else
          {
              theStack.push( element );
          }
        }
        element = theStack.pop().toString();

        //if it's a number with enough characters to potentially be a float
        //and the last two characters are '.' followed by '0'
        //then you may as well strip them off and display it as an int
        if( element.length() > 2 && element.charAt( element.length()-1 ) == '0' && element.charAt( element.length()-2 ) == '.' )
        {
            element = element.substring( 0, element.length()-2 );
        }
        
        value = value + " = " + element;
        return value;
    }
    
    
    /********************************************************************************
      Method: isOpen()
      Description: checks for a open braket or paranthesis
        not so important when only one set of [] can be sent
      Parameters: char token
      Used by: StringMath.convert()
    ********************************************************************************/
    private boolean isOpen( char token )
    {
        if(  token == '('  || token == '[' )
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    
    /********************************************************************************
      Method: isClose()
      Description: checks for a close braket or paranthesis
        not so important when only one set of [] can be sent
      Parameters: char token
      Used by: StringMath.convert()
    ********************************************************************************/
    private boolean isClose( char token )
    {
        if(  token == ')'  || token == ']' )
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    
    /********************************************************************************
      Method: getPrecedence()
      Description: assigns a precedence value to an operator
      Parameters: char operator
      Used by: StringMath.convert()
    ********************************************************************************/
    private int getPrecedence( char operator )
    {
        if( operator == '+' || operator == '-')
        {
            return 1;
        }
        else if( operator == '*' || operator =='/' )
        {
            return 2;
        }
        else if( operator == 'd' )
        {
            return 3;
        }
        else
        {
             return 0;
        }
        
    }
    
    
    /********************************************************************************
      Method: isOperator()
      Description: checks if a character is an operator
      Parameters: char token
      Used by: StringMath.convert()
    ********************************************************************************/
    private boolean isOperator(char token)
    {
        if( token == '*' ||  token == '/' ||  token == '+' || token == '-' ||  token == 'd'   )
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    
    /********************************************************************************
      Method: isNumber()
      Description: checks if a character is a number
      Parameters: char token
      Used by: StringMath.convert()
    ********************************************************************************/
    private boolean isNumber(char token)
    {
        if( token == '0' || token == '1' ||  token == '2' ||  token == '3' || token == '4' ||  token == '5' || token == '6' || token == '7' || token == '8' || token == '9' || token == '.')
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    
    /**
     * The public portal to StringMath.
     * @param expression an infix math expression
     * @return solution passed from evaluatePostfix
     */
    public String eval( String expression )
    {
      String temp;
      String trimmedString = expression.trim();
      
      if(trimmedString.length() < 3)
    	  throw new ArithmeticException("empty expression");
      
      
      try
      {
        temp = convert( trimmedString );
        temp = evaluatePostfix( temp );
      } 
      catch( ArithmeticException ae )
      {
          throw new ArithmeticException(ae.getMessage());
          
      }
      return temp;
    }
    
    
    /**
     * Creates a new instance of StringMath
     */
    public StringMath() 
    {
    }
    
}
